/*
 * AbsController.java
 *
 * Created on 23 October 2007, 22:41
 *
 */

package net.ubinow.mflex.baseui;

import java.util.Hashtable;

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.midlet.MIDlet;

import net.ubinow.mflex.data.IntQueue;
import net.ubinow.mflex.events.EventFactory;
import net.ubinow.mflex.util.Constants;
import net.ubinow.mflex.util.Logging;
import net.ubinow.mflex.util.MiscUtil;
import net.ubinow.mflex.util.StringUtil;



/**
 * the skeleton to be used as the applications controller - will handle
 * view stacking, session management, resource management, cycle timing,
 * input and output buffering
 * @author Josh
 */
public abstract class AbsController extends Canvas implements Runnable {
    
    protected static final boolean RECORD_FPS = true; 
    
    public static final int STATE_LOADING_RESOURCES     = 0;
    public static final int STATE_DISPLAYING_VIEW       = 1;
    
    //<editor-fold defaultstate="collapsed" desc="private fields">
    private static final int MAX_FPS = 30;
    
    private Image _shadeTile = null; // tile used to draw over the screen to provide the effect of transparency
    private int _yTiles4Shading = 0; // number of tiles to cover the height of the screen
    private int _xTiles4Shading = 0; // number of tiles to cover the width of the screen
    private Sprite _loadingSprite = null;
    private int _loadingAnimationFrameTime = 0;
    private long _loadingAnimationFrameTimeStamp = 0;
    
    private Thread _applicationThread = null;
    private boolean _running = false;
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="pre-calculated values">
    
    public static int gScreenWidth = 0;
    public static int gScreenHeight = 0;
    
    /** half of the screens width **/
    public static int gScreenWidthM = 0;
    /** half of the screens height **/
    public static int gScreenHeightM = 0;
    
    //</editor-fold>
    
    protected MIDlet _midlet = null;
    protected int _currentState = 0;
    protected IntQueue _keyPressedQ = null;
    protected IntQueue _keyPressedActionQ = null;
    protected IntQueue _keyReleasedQ = null;
    protected IntQueue _keyReleasedActionQ = null;
    
    protected WFont _defaultFont = null; 
    
    // singleton classes - just for quick reference 
    protected Logging _log = null;
    protected MiscUtil _util = null;
    protected StringUtil _stringUtil = null; 
    protected EventFactory _eventFactory = null;
    
    protected int _fps = 0;
    private int _framesThisSecond = 0;
    private long _fpsTimeStamp = 0;
    
    //<editor-fold defaultstate="collapsed" desc="constructor" >
    /** Creates a new instance of AbsController */
    public AbsController( MIDlet midlet, Image shadingTile, Image loadingImage, int screenHeight, int screenWidth ) {
        this( midlet, shadingTile, loadingImage, 0, 0, 0, screenWidth, screenHeight );
    }
    
    public AbsController( MIDlet midlet, Image shadingTile, Image loadingImage, int loadingImageW,
            int loadingImageH, int loadingAnimFrameTime, int screenWidth, int screenHeight ) {
        
        setFullScreenMode(true);
        
        _midlet = midlet;
        _shadeTile = shadingTile;
        if( loadingImageW == 0 ){
            _loadingSprite = new Sprite( loadingImage );
        } else{
            _loadingSprite = new Sprite( loadingImage, loadingImageW, loadingImageH );
            _loadingAnimationFrameTime = loadingAnimFrameTime;
        }
        
        loadingImage = null;
        
        gScreenHeight = screenHeight;
        gScreenWidth = screenWidth;
        
        init();
    }
    
    private void init(){
        // set state to loading
        _currentState = STATE_LOADING_RESOURCES;
        
        // get reference to the singleton helper classes - TODO: find out whether it is more efficent to have lazy loading or instantiating them inititally
        _log = Logging.getInstance();
        _util = MiscUtil.getInstance();
        _stringUtil = StringUtil.getInstance(); 
        _eventFactory = EventFactory.getInstance();
        
        // work out coordinates for the loading animation
        int x = (gScreenWidth - _loadingSprite.getWidth())>>1;
        int y = (gScreenHeight - _loadingSprite.getHeight())>>1;
        _loadingSprite.setPosition( x, y );
        
        // work out number of sading tiles required
        _yTiles4Shading = gScreenHeight/_shadeTile.getHeight();
        _xTiles4Shading = gScreenWidth/_shadeTile.getWidth();
        
        initPreCalcVar();       
        
        // set up the queues to hold the key events
        _keyPressedQ = new IntQueue( 10 );
        _keyPressedActionQ = new IntQueue( 10 );
        _keyReleasedQ = new IntQueue( 10 );
        _keyReleasedActionQ = new IntQueue( 10 );
        
        // kick off thread
        _running = true;
        _applicationThread = new Thread( this );
        _applicationThread.start();
        
        _log.trace( Logging.LOG_LEVEL_INFO, "AbsController.init - complete" );
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="init methods">
    
    /**
     * init all resources - SHOULD CALLED BY THE MIDLET so it runs under the midlets thread and not the applications
     * (just so we can see a loading animation)
     **/
    public void initResources(){        
        
        initIViewQ();
        initSessionQ();                 
        
        initData();
        initImages();
        initFonts();
        initFirstPage();
        
        _log.trace( Logging.LOG_LEVEL_INFO, "AbsController.initResources - complete" );
        
        // show the first page 
       _currentState = STATE_DISPLAYING_VIEW;
    }
    
    /** load up data files (languages resources, etc ) **/
    public abstract void initData();
    
    /** load images **/
    public abstract void initImages();
    
    /** load fonts **/
    public abstract void initFonts();        
    
    /** load and set the first page - once this is set the state will change and the animation will disappear **/
    public abstract void initFirstPage();
    
    //</editor-fold>
    
    // main application thread
    public void run() {
        _loadingAnimationFrameTimeStamp = System.currentTimeMillis();
        long currTime = System.currentTimeMillis();
        _fpsTimeStamp = System.currentTimeMillis();
        long endTime;
            
        try{ // TODO: remove try-catch block for main loop
        while( _running ){
            long elapsedTime = System.currentTimeMillis() - currTime;
            currTime += elapsedTime;
            
            if( _pause ){
                synchronized( this ){
                    try{
                        this.wait();
                    } catch( Exception e ){}
                }
            }
            
            if( _pause )
                continue;
            
            if( !_running )
                break;
            
            // *** handle input
            // key pressed
            if( _keyPressedQ.peek() != Constants.NULL_INT ){
                handleKeyPressed( _keyPressedActionQ.dequeue(), _keyPressedQ.dequeue() );
            }
            
            // key released
            if( _keyReleasedQ.peek() != Constants.NULL_INT ){
                handleKeyReleased( _keyReleasedActionQ.dequeue(), _keyReleasedQ.dequeue() );
            }
            
            // *** handle cycle
            absCycle();            
            
            // *** render
            repaint();
            serviceRepaints();
            
            if( RECORD_FPS ){
                if( (System.currentTimeMillis()-_fpsTimeStamp) >= 1000 ){
                    _fps = _framesThisSecond;
                    _framesThisSecond = 0;
                    _fpsTimeStamp = System.currentTimeMillis();
                }
                else{
                    _framesThisSecond++;
                }
            }
            
            paceMaker( currTime );
        }
        }
        catch( Exception e ){
            e.printStackTrace();
        }
        
        cleanup();
        
        // kill application
    }
    
    /** when the thread ends this method gives the application a chance to clean up ie save data etc **/
    protected abstract void cleanup();
    
    //<editor-fold defaultstate="collapsed" desc="input handling methods">
    
    protected long _keyPressedTimeStamp = 0;
    protected long _keyReleasedTimeStamp = 0;
    
    public void keyPressed(int keyCode) {               
        
        if( _inputLock )
            return;
        
        _keyPressedTimeStamp = System.currentTimeMillis();
        
        _keyPressedQ.queue( keyCode );
        _keyPressedActionQ.queue( getGameAction( keyCode ) );
        
        /*g_nKeyCode = keyCode;
        g_nKeyDirCode = getKeyDirection(keyCode);
        keyState = KEY_PRESSED;
        tmrCnt_upDown_20ps = 0;*/        
    }
    
    /** Key released */
    public void keyReleased(int keyCode) {        
        _keyReleasedTimeStamp = System.currentTimeMillis();
        
        _keyReleasedQ.queue( keyCode );
        _keyReleasedActionQ.queue( getGameAction( keyCode ) );
        
        /*keyState = KEY_RELEASED;
        keyDownCnt = 0;*/
        
    }
    
    public abstract void handleKeyPressed( int action, int keyCode );
    public abstract void handleKeyReleased( int action, int keyCode );
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="cycle methods - the labour">
    
    /** abstract cycle method to handle view transitions etc **/
    private void absCycle(){
        // TODO: some work needs to be done here to handle view transitions etc
        
        if( getPreviousView() != null && getPreviousView().isVisible() ){
            getPreviousView().cycle(); 
        }
        
        if( getCurrentView() != null && getCurrentView().isVisible() ){
            getCurrentView().cycle(); 
        }
        
        if( hasDialog() && getDialog().isVisible() ){
            getDialog().cycle(); 
        }        
        
        cycle();
    }
    
    public abstract void cycle();
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="rendering methods">
    
    protected void paint( Graphics g ) {
        // clear current screen 
        g.setColor( Constants.COLOUR_WHITE );
        g.fillRect( 0, 0, gScreenWidth, gScreenHeight );
        
        if( _currentState == STATE_LOADING_RESOURCES ){
            g.setColor( 0x000000 );
            g.drawRect( 0, 0, gScreenWidth, gScreenHeight );
            
            renderLoadingScreen( g );            
        } else{

            // display previous screen?
            if( getPreviousView() != null && getPreviousView().isVisible() ){
                getPreviousView().paint( g, 0, 0, 1 );
            }
            // display the current view
            if( getCurrentView() != null && getCurrentView().isVisible() ){
                getCurrentView().paint( g, 0, 0, 1 );
            }
            
            if( hasDialog() && getDialog().isVisible() ){
                getDialog().paint( g, 0, 0, 1 );
            }
            
            if( _loading ){
                renderLoadingScreen( g );
            }                        
        }
        
        // print frames per second 
        if( RECORD_FPS ){
            g.setColor( 255, 0, 0 );
            g.drawString( "FPS: " + _fps, getWidth()-60, getHeight()-40, Constants.GFX_ALIGN_TOP_LEFT );
        }
    }
    
    private void renderLoadingScreen( Graphics g ){
        // render shaded tile
        
        for (int y = 0; y < _yTiles4Shading; y++) {
            for (int x = 0; x < _xTiles4Shading; x++) {
                g.drawImage( _shadeTile, x * _shadeTile.getWidth(), y * _shadeTile.getHeight(), Constants.GFX_ALIGN_TOP_LEFT );;
            }
        }
        
        // render loading animation
        if( (System.currentTimeMillis() - _loadingAnimationFrameTimeStamp) >= _loadingAnimationFrameTime ){
            _loadingAnimationFrameTimeStamp = System.currentTimeMillis();
            _loadingSprite.nextFrame();
        }
        _loadingSprite.paint( g );
        
        // TODO: render status
        
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="thread management">
    
    private boolean _pause = false;
    
    protected void exit(){
        _running = false;
    }
    
    protected void pause(){
        _pause = true;
    }
    
    public void resume(){
        _pause = false;
        
        synchronized( this ){
            try{
                this.notify();
            } catch( Exception e ){}
        }
    }
    
    public boolean isPaused(){
        return _running && _pause && (_applicationThread != null && _applicationThread.isAlive());
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="busy/loading fields and methods">
    
    private boolean _loading = false;
    private boolean _inputLock = false;
    private String _loadingStatus = "";
    
    public void setLoading( String status, boolean lockKeys ){
        _loading = true;
        _loadingAnimationFrameTimeStamp = System.currentTimeMillis();
        _loadingSprite.setFrame( 0 );
        
        setInputLock( lockKeys );
    }
    
    public void updateLoadingStatus( String status ){
        // TODO: do I need to sync _loadingStatus?
        _loadingStatus = status;
    }
    
    public void setInputLock( boolean lock ){
        _inputLock = lock;
    }
    
    public void releaseLoading(){
        // TODO:
        
        _loading = false;
        _loadingStatus = "";
        setInputLock( false );
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="font fields and methods">
    
    // TODO: allow for multiple fonts            
    
    /** set the default font - should be called from the initFonts method **/
    public void initDefaultFont( WFont defaultFont ){
        _defaultFont = defaultFont; 
    }        
    
    public WFont getDefaultFont(){
        return _defaultFont;
    }
    
    /** draw with the default font **/
    public void drawStringWithDF( Graphics g, String txt, int x, int y ){
        if( _defaultFont != null ){
            _defaultFont.drawString( g, txt, x, y );
        }
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="view fields and methods">            
    
    public IStatefulView getCurrentView(){
        if( iviewQIsEmpty() )
            return null;
        
        int qindex = _iviewQNext-1;
        
        if( qindex < 0 )
            qindex = _iviewQ.length-1;
        
        return (IStatefulView)_iviews.get( _iviewQ[qindex] );
    }
    
    public IStatefulView getPreviousView(){
        if( iviewQIsEmpty() )
            return null;
        
        if( _iviewQFirst == _iviewQNext-2 )
            return null;
        
        int qindex = _iviewQNext-2;
        
        if( qindex < 0 )
            qindex = _iviewQ.length-1;
        
        if( _iviewQ[qindex] == null )
            return null;
        
        return (IStatefulView)_iviews.get( _iviewQ[qindex] );
    }
    
    public IWidget getViewByID( String key ){
        if( _iviews.containsKey( key ) )
            return (IWidget)_iviews.get( key );
        else
            return null;
    }
    
    public void setNextViewToPrevious(){
        IStatefulView previous = getPreviousView(); 
        
        if( previous == null )
            return;
        
        setNextView(previous.getID(), previous);
    }
    
    public void setNextView( String key, IStatefulView view ){

        // defensive programming... if the ID is not set then we'll set it to the key value
        if( view.getID().equals( Constants.UNDEFINED ) ){
            view.setID( key );
        }
        // remember the last view to associate that view with the new one
        String previousKey = iviewQPeek();
        IStatefulView previousView = null;
        if( previousKey != null ){
            previousView = (IStatefulView)_iviews.get( previousKey );
            view.setPreviousView( previousView );
        }
        
        if( _iviews.containsKey( key ) ){
                iviewQPrioritize( key );
        } else{
            String keyToRemove = iviewQInsert( key );
            
            if( keyToRemove != null ){
                if( _iviews.containsKey( keyToRemove ) ){
                    _iviews.remove( keyToRemove );
                }
            }
        }
                
        Object throwAway = _iviews.put( key, view );        
        
        if( previousView != null )
            previousView.setFocus( false , -1 );
        
        view.setFocus( true,  -1 );
        
        _log.trace( Logging.LOG_LEVEL_INFO, "AbsController.setNextView; added IView " + key );                
    }
        
    
    //<editor-fold defaultstate="collapsed" desc="priority queue for the IViews">
    
    private Hashtable _iviews   = null;
    private String[] _iviewQ    = null;
    private int _iviewQFirst    = 0;
    private int _iviewQNext     = 0;
    
    private void initIViewQ(){
        if( _iviewQ == null )
            _iviewQ = new String[Constants.AD_QUEUE_SIZE];
        
        _iviewQFirst = _iviewQNext = 0;
        
        if( _iviews == null )
            _iviews = new Hashtable( Constants.AD_QUEUE_SIZE );
        
        _log.trace( Logging.LOG_LEVEL_INFO, "AbsController.initViewQ - complete" );                
    }
    
    private boolean iviewQIsEmpty(){
        return _iviewQFirst == _iviewQNext;
    }
    
    /** move the key to the end of the queue (so it is not removed);  **/
    private void iviewQPrioritize( String key ){
        int index = iviewQGetIndex( key );
        
        // not found then return
        if( index == -1 )
            return;
        
        // already last in the queue
        if( index == _iviewQNext-1 )
            return;
        
        int i = index;
        int iterations = 0; // make sure we don't get away on ourselves.
        
        while( i != _iviewQNext-1 && iterations < _iviewQ.length-1 ){
            // some paranoid programming
            if( _iviewQ[i] == null ){
                iterations++;
                i++;
                continue;
            }
            
            // move records back
            if( i > _iviewQ.length-1 )
                i = 0;
            
            int nextVal = i+1;
            
            if( nextVal > _iviewQ.length-1 )
                nextVal = 0;
            
            _iviewQ[i] = _iviewQ[nextVal];
            
            iterations++;
            i++;
        }
        
        // amend to the end of the queue
        _iviewQ[_iviewQNext-1] = key;
        
    }
    
    private int iviewQGetIndex( String key ){
        if( iviewQIsEmpty() )
            return -1;
        
        int iterations = 0; // make sure we don't get away on ourselves.
        int i = _iviewQFirst;
        
        while( i != _iviewQNext-1 && iterations < _iviewQ.length-1 ){
            if( _iviewQ[i] == null ){
                iterations++;
                i++;
                continue;
            }
            
            if( _iviewQ[i].equals( key ) )
                return i;
            
            if( i > _iviewQ.length-1 )
                i = 0;
            
            iterations++;
            i++;
        }
        
        return -1;
    }
    
    /**
     *
     * @param key
     * @return returns the key value if the queue was full; if this is not null then use this value to remove it from the hashtable
     */
    private String iviewQInsert( String key ){
        _iviewQ[_iviewQNext] = key;
        _iviewQNext++;
        
        if( _iviewQNext >= _iviewQ.length )
            _iviewQNext = 0;
        
        if( _iviewQNext == _iviewQFirst ){
            // queue is full therefore we will move the first pointer forward 1 and return its value
            String res = _iviewQ[_iviewQFirst];
            _iviewQFirst++;
            
            return res;
        } else{
            return null;
        }
    }
    
    /** pop the first item from the queue **/
    private String ivewQRemove(){
        if( iviewQIsEmpty() )
            return null;
        
        String key = _iviewQ[_iviewQFirst];
        _iviewQFirst++;
        
        return key;
        
    }
    
    /** peeks at the first item from the queue **/ 
    private String iviewQPeek(){
        if( iviewQIsEmpty() )
            return null;
        
        String key = _iviewQ[_iviewQFirst];       
        return key;
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="dialog view">
    
    private IStatefulView _dialogView = null;
    
    protected IStatefulView getDialog(){
        return _dialogView;
    }
    
    protected void setDialog( IStatefulView dialog ){
        _dialogView = dialog; 
    }
    
    protected boolean hasDialog(){
        return _dialogView != null;
    }
    
    protected boolean isDialogShowing(){
        if( hasDialog() && getDialog().isVisible() )
            return true;
        else
            return false; 
    }
    
    protected void clearDialog(){
        _dialogView = null;
    }
    
    //</editor-fold>
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="session management methods">    
    
    ///
    // session management is a sets that allow the controller to cache data items between view changes; thus 
    // not having to request that information each time 
    ///
    
    // TODO: fully implement this method section
    
    private Hashtable _sessionData = null; 
    private String[] _sessionQ = null; 
    private int _sessionQFirst = 0;
    private int _sessionQNext = 0; 
    
    private void initSessionQ(){
        clearSessionQ(); 
    }
    
    protected void clearSessionQ(){
        if( _sessionData != null )
            _sessionData.clear(); 
        else
            _sessionData = new Hashtable( Constants.AD_QUEUE_SIZE ); 
        
        _sessionQ = new String[Constants.AD_QUEUE_SIZE];        
        _sessionQFirst = _sessionQNext = 0;        
    }
    
    protected Object getSessionData( String key ){
        if( _sessionData.containsKey( key ) ){
            return _sessionData.get( key );
        }
        else{
            return null;
        }
    }   
    
    protected void addSessionData( String key, Object obj ){
        if( _sessionData.containsKey( key ) ){
            _sessionData.put( key, obj ); 
        }
        else{
            String keyToRemove = sessionQInsert( key );
            
            if( keyToRemove != null ){
                if( _sessionData.containsKey( keyToRemove ) ){
                    _sessionData.remove( keyToRemove );
                }
            }
            
            _sessionData.put( key, obj ); 
        }
    }
    
    /**
     *
     * @param key
     * @return returns the key value if the queue was full; if this is not null then use this value to remove it from the hashtable
     */
    private String sessionQInsert( String key ){
        _sessionQ[_sessionQNext] = key;
        _sessionQNext++;
        
        if( _sessionQNext >= _sessionQ.length )
            _sessionQNext = 0;
        
        if( _sessionQNext == _sessionQFirst ){
            // queue is full therefore we will move the first pointer forward 1 and return its value
            String res = _sessionQ[_sessionQFirst];
            _sessionQFirst++;
            
            return res;
        } else{
            return null;
        }
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="private helper methods">
    
    /** method used to regulate the speed of the application **/
    private void paceMaker( long currTime ){
        // sleep if necessary to make a smooth framerate
        long endTime = System.currentTimeMillis() - currTime;
        
        if (endTime < (1000 / MAX_FPS)) {
            try {
                // frames per second
                _applicationThread.sleep((1000 / MAX_FPS) - endTime);
            } catch (Exception e) { }
        } else {
            try {
                _applicationThread.yield();
            } catch (Exception e) { }
        }
    }
    
    /** method used to set up all of the global helper methods **/
    private void initPreCalcVar(){
        gScreenWidthM = gScreenWidth>>1;
        gScreenHeightM = gScreenHeight>>1;
    }
    
    //</editor-fold>
    
}
